#!/usr/bin/env python2
# 主机端入口
#
# Copyright (C) 2016-2020  Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
# optparse: 是一个命令行参数配置及解析库
import sys, os, gc, optparse, logging, time, collections, importlib
import util, reactor, queuelogger, msgproto
import gcode, configfile, pins, mcu, toolhead, webhooks

message_ready = "Printer is ready"

#HOST程序启动消息提示
message_startup = """
Printer is not ready
The klippy host software is attempting to connect.  Please
retry in a few moments.
"""

message_restart = """
Once the underlying issue is corrected, use the "RESTART"
command to reload the config and restart the host software.
Printer is halted
"""

message_protocol_error1 = """
This is frequently caused by running an older version of the
firmware on the MCU(s). Fix by recompiling and flashing the
firmware.
"""

message_protocol_error2 = """
Once the underlying issue is corrected, use the "RESTART"
command to reload the config and restart the host software.
"""

message_mcu_connect_error = """
Once the underlying issue is corrected, use the
"FIRMWARE_RESTART" command to reset the firmware, reload the
config, and restart the host software.
Error configuring printer
"""

message_shutdown = """
Once the underlying issue is corrected, use the
"FIRMWARE_RESTART" command to reset the firmware, reload the
config, and restart the host software.
Printer is shutdown
"""

class Printer:
    # 全局通用的配置错误类
    config_error = configfile.error
    # 全局通用的Gcode命令相关的错误类
    command_error = gcode.CommandError
    def __init__(self, main_reactor, bglogger, start_args):
        """
        main_reactor:主事件循环及定时器处理器
        bglogger:日志记录组件
        start_args:启动参数
        """
        self.bglogger = bglogger
        self.start_args = start_args
        self.reactor = main_reactor
        # 使用异步(协程)方式创建与下位机的连接,主循环启动后_connect便会执行
        self.reactor.register_callback(self._connect)
        # 当前状态消息提示信息
        self.state_message = message_startup
        # 当前是否是shutdown状态
        self.in_shutdown_state = False
        # 启动结果
        self.run_result = None
        self.event_handlers = {}
        # Printer对象中存储了系统所有用到的模块对象，通过getobject函数可以获取对应的模块对象
        self.objects = collections.OrderedDict()
        # 在加载配置文件之前的设置，添加gcode和web相关的服务模块
        for m in [gcode, webhooks]:
            m.add_early_printer_objects(self)
    def get_start_args(self):
        return self.start_args
    def get_reactor(self):
        return self.reactor
    def get_state_message(self):
        """
        获取状态描述信息和状态类型的元组
        """
        if self.state_message == message_ready:
            category = "ready"
        elif self.state_message == message_startup:
            category = "startup"
        elif self.in_shutdown_state:
            category = "shutdown"
        else:
            category = "error"
        return self.state_message, category
    def is_shutdown(self):
        return self.in_shutdown_state
    def _set_state(self, msg):
        """
        设置打印机状态
        """
        if self.state_message in (message_ready, message_startup):
            self.state_message = msg
        if (msg != message_ready
            and self.start_args.get('debuginput') is not None):
            self.request_exit('error_exit')
    def add_object(self, name, obj):
        """
        添加服务模块
        """
        if name in self.objects:
            raise self.config_error(
                "Printer object '%s' already created" % (name,))
        self.objects[name] = obj
    def lookup_object(self, name, default=configfile.sentinel):
        """
        查找服务模块
        """
        if name in self.objects:
            return self.objects[name]
        if default is configfile.sentinel:
            raise self.config_error("Unknown config object '%s'" % (name,))
        return default
    def lookup_objects(self, module=None):
        """
        通过模块名查找服务模块列表，包括以模块名为前缀的模块
        """
        if module is None:
            return list(self.objects.items())
        prefix = module + ' '
        objs = [(n, self.objects[n])
                for n in self.objects if n.startswith(prefix)]
        if module in self.objects:
            return [(module, self.objects[module])] + objs
        return objs
    def load_object(self, config, section, default=configfile.sentinel):
        """
        根据config section添加对应的服务组件，服务组件均在extras目录下
        config:section对应的配置信息
        section:配置文件的节名称
        """
        # 组件已加载则不重复加载,直接返回对象实例
        if section in self.objects:
            return self.objects[section]
        module_parts = section.split()
        module_name = module_parts[0]
        # 以单文件的方式提供服务
        py_name = os.path.join(os.path.dirname(__file__),
                               'extras', module_name + '.py')
        # 以目录的形式提供服务，查找目录下的__init__.py文件
        py_dirname = os.path.join(os.path.dirname(__file__),
                                  'extras', module_name, '__init__.py')
        if not os.path.exists(py_name) and not os.path.exists(py_dirname):
            if default is not configfile.sentinel:
                return default
            raise self.config_error("Unable to load module '%s'" % (section,))
        # 导入对应的模块
        mod = importlib.import_module('extras.' + module_name)
        # 初始化函数名
        init_func = 'load_config'
        if len(module_parts) > 1:
            init_func = 'load_config_prefix'
        # 在模块中查找对应的函数名
        init_func = getattr(mod, init_func, None)
        if init_func is None:
            if default is not configfile.sentinel:
                return default
            raise self.config_error("Unable to load module '%s'" % (section,))
        # 调用初始化函数获取服务对象
        self.objects[section] = init_func(config.getsection(section))
        return self.objects[section]
    def _read_config(self):
        """
        读取上位机配置文件数据，生成ConfigWrapper对象，根据配置文件中的section配置添加对应的服务对象
        """
        # configfile也是服务模块之一
        self.objects['configfile'] = pconfig = configfile.PrinterConfig(self)
        # 配置文件中printer段的配置信息
        config = pconfig.read_main_config()
        if self.bglogger is not None:
            pconfig.log_config(config)
        # 添加pin口解析和MCU服务组件
        # 加载pins组件时只是实例化了PrinterPins 类变量，其他啥也没干
        # 加载mcu组件时实例化了MCU类，同时注册了如下的事件监听
        # klippy:firmware_restart
        # klippy:mcu_identify
        # klippy:connect
        # klippy:shutdown
        # klippy:disconnect
        for m in [pins, mcu]:
            m.add_printer_objects(config)
        # 根据不同的section添加extras目录下的对应的服务模块,大部分模块都在此处被加载，模块的初始化也从此时开始
        # 重复的和不在extras目录下的组件不加载
        # 包括如下模块：
        # heater_bed 热床，加载任何加热器都会触发默认支持的所有传感器模块的加载，比如ds18b20和temperatur_host等传感器
        #            另外，还会导致verify_heater和pid_calibrate模块的加载
        # fan 风扇
        # heater_fan 加热器风扇
        # idle_timeout 空闲定时
        # probe 探头
        # tmcxxxx 比如tmc2209驱动
        # safe_z_home z轴安全归位
        # z_tilt z轴倾斜调平
        # board_pins 引脚定义
        # display 下位机显示屏（使用目录的方式组织代码）
        # output_pin 配置一些杂项的输出引脚
        # neopixel WS2812彩灯控制
        # gcode_macro 自定义的GCode宏代码
        # pause_resume 暂停、恢复功能，基础位置然后从该位置恢复打印
        for section_config in config.get_prefix_sections(''):
            self.load_object(config, section_config.get_name(), None)
        # 加载toolhead模块,配置文件中没有toolhead相关的配置，因此需要单独加载
        for m in [toolhead]:
            m.add_printer_objects(config)
        # 检查未使用到的参数
        pconfig.check_unused_options(config)
    def _build_protocol_error_message(self, e):
        """
        创建协议错误消息
        """
        host_version = self.start_args['software_version']
        msg_update = []
        msg_updated = []
        for mcu_name, mcu in self.lookup_objects('mcu'):
            try:
                mcu_version = mcu.get_status()['mcu_version']
            except:
                logging.exception("Unable to retrieve mcu_version from mcu")
                continue
            if mcu_version != host_version:
                msg_update.append("%s: Current version %s"
                                  % (mcu_name.split()[-1], mcu_version))
            else:
                msg_updated.append("%s: Current version %s"
                                   % (mcu_name.split()[-1], mcu_version))
        if not msg_update:
            msg_update.append("<none>")
        if not msg_updated:
            msg_updated.append("<none>")
        msg = ["MCU Protocol error",
               message_protocol_error1,
               "Your Klipper version is: %s" % (host_version,),
               "MCU(s) which should be updated:"]
        msg += msg_update + ["Up-to-date MCU(s):"] + msg_updated
        msg += [message_protocol_error2, str(e)]
        return "\n".join(msg)
    def _connect(self, eventtime):
        """
        连接到下位机控制板
        """
        try:
            # 读取配置文件，根据配置文件添加服务模块,程序99%的逻辑从这行代码开始
            self._read_config()
            # 所有组件加载完成后，发送mcu_identify事件
            self.send_event("klippy:mcu_identify")
            # 然后发送klippy:connect事件
            for cb in self.event_handlers.get("klippy:connect", []):
                if self.state_message is not message_startup:
                    return
                cb()
        except (self.config_error, pins.error) as e:
            logging.exception("Config error")
            self._set_state("%s\n%s" % (str(e), message_restart))
            return
        except msgproto.error as e:
            logging.exception("Protocol error")
            self._set_state(self._build_protocol_error_message(e))
            util.dump_mcu_build()
            return
        except mcu.error as e:
            logging.exception("MCU error during connect")
            self._set_state("%s%s" % (str(e), message_mcu_connect_error))
            util.dump_mcu_build()
            return
        except Exception as e:
            logging.exception("Unhandled exception during connect")
            self._set_state("Internal error during connect: %s\n%s"
                            % (str(e), message_restart,))
            return
        try:
            self._set_state(message_ready)
            for cb in self.event_handlers.get("klippy:ready", []):
                if self.state_message is not message_ready:
                    return
                cb()
        except Exception as e:
            logging.exception("Unhandled exception during ready callback")
            self.invoke_shutdown("Internal error during ready callback: %s"
                                 % (str(e),))
    def run(self):
        """
        启动主轮训
        """
        # 获取以秒为单位的时间戳
        systime = time.time()
        monotime = self.reactor.monotonic()
        logging.info("Start printer at %s (%.1f %.1f)",
                     time.asctime(time.localtime(systime)), systime, monotime)
        # 进入事件循环，在协程中运行
        try:
            self.reactor.run()
        except:
            msg = "Unhandled exception during run"
            logging.exception(msg)
            # Exception from a reactor callback - try to shutdown
            try:
                self.reactor.register_callback((lambda e:
                                                self.invoke_shutdown(msg)))
                self.reactor.run()
            except:
                logging.exception("Repeat unhandled exception during run")
                # Another exception - try to exit
                self.run_result = "error_exit"
        # Check restart flags
        run_result = self.run_result
        try:
            if run_result == 'firmware_restart':
                self.send_event("klippy:firmware_restart")
            self.send_event("klippy:disconnect")
        except:
            logging.exception("Unhandled exception during post run")
        return run_result
    def set_rollover_info(self, name, info, log=True):
        if log:
            logging.info(info)
        if self.bglogger is not None:
            self.bglogger.set_rollover_info(name, info)
    def invoke_shutdown(self, msg):
        if self.in_shutdown_state:
            return
        logging.error("Transition to shutdown state: %s", msg)
        self.in_shutdown_state = True
        self._set_state("%s%s" % (msg, message_shutdown))
        for cb in self.event_handlers.get("klippy:shutdown", []):
            try:
                cb()
            except:
                logging.exception("Exception during shutdown handler")
        logging.info("Reactor garbage collection: %s",
                     self.reactor.get_gc_stats())
    def invoke_async_shutdown(self, msg):
        self.reactor.register_async_callback(
            (lambda e: self.invoke_shutdown(msg)))
    def register_event_handler(self, event, callback):
        """
        注册事件处理函数
        """
        self.event_handlers.setdefault(event, []).append(callback)
    # 发送指定事件并触发对应i事件处理函数的回调
    def send_event(self, event, *params):
        """
        发送指定事件，实际上就是执行对应的事件处理函数列表
        """
        return [cb(*params) for cb in self.event_handlers.get(event, [])]
    def request_exit(self, result):
        if self.run_result is None:
            self.run_result = result
        self.reactor.end()


######################################################################
# Startup
######################################################################

def import_test():
    # Import all optional modules (used as a build test)
    dname = os.path.dirname(__file__)
    for mname in ['extras', 'kinematics']:
        for fname in os.listdir(os.path.join(dname, mname)):
            if fname.endswith('.py') and fname != '__init__.py':
                module_name = fname[:-3]
            else:
                iname = os.path.join(dname, mname, fname, '__init__.py')
                if not os.path.exists(iname):
                    continue
                module_name = fname
            importlib.import_module(mname + '.' + module_name)
    sys.exit(0)

def arg_dictionary(option, opt_str, value, parser):
    key, fname = "dictionary", value
    if '=' in value:
        mcu_name, fname = value.split('=', 1)
        key = "dictionary_" + mcu_name
    if parser.values.dictionary is None:
        parser.values.dictionary = {}
    parser.values.dictionary[key] = fname

# 》》》程序入口《《《
def main():
    # 命令行参数配置及解析，usage提示命令的使用方法
    usage = "%prog [options] <config file>"
    opts = optparse.OptionParser(usage)
    # 配置命令行可用参数及其默认值
    # 从文件读取命令而不是从tty端口读取，用于输入调试
    opts.add_option("-i", "--debuginput", dest="debuginput",
                    help="read commands from file instead of from tty port")
    opts.add_option("-I", "--input-tty", dest="inputtty",
                    default='/tmp/printer',
                    help="input tty name (default is /tmp/printer)")
    # api服务器socket名称
    opts.add_option("-a", "--api-server", dest="apiserver",
                    help="api server unix domain socket filename")
    # 日志记录写入文件
    opts.add_option("-l", "--logfile", dest="logfile",
                    help="write log to file instead of stderr")
    # 显示调试日志
    opts.add_option("-v", action="store_true", dest="verbose",
                    help="enable debug messages")
    # 将输出写入文件而不是串行端口，用于输出调试
    opts.add_option("-o", "--debugoutput", dest="debugoutput",
                    help="write output to file instead of to serial port")
    # 协议字典数据通常情况下是存放在下位机字典中的，在此可以指定从host文件中读取
    opts.add_option("-d", "--dictionary", dest="dictionary", type="string",
                    action="callback", callback=arg_dictionary,
                    help="file to read for mcu protocol dictionary")
    opts.add_option("--import-test", action="store_true",
                    help="perform an import module test")
    #解析命令行参数
    options, args = opts.parse_args()
    if options.import_test:
        import_test()
    if len(args) != 1:
        opts.error("Incorrect number of arguments")
    # Printer类的启动参数
    start_args = {'config_file': args[0], 'apiserver': options.apiserver,
                  'start_reason': 'startup'}

    # 日志输出等级，默认是INFO等级，只有添加了-v参数才能显示DEBUG消息
    debuglevel = logging.INFO
    if options.verbose:
        debuglevel = logging.DEBUG
    if options.debuginput:
        # 从文件读取命令，打开文件并设置文件描述符
        start_args['debuginput'] = options.debuginput
        debuginput = open(options.debuginput, 'rb')
        start_args['gcode_fd'] = debuginput.fileno()
    else:
        # 创建/tmp/printer伪设备
        start_args['gcode_fd'] = util.create_pty(options.inputtty)
    if options.debugoutput:
        start_args['debugoutput'] = options.debugoutput
        start_args.update(options.dictionary)
    # 设置日志输出文件，默认是/tmp/klipper.log
    bglogger = None
    if options.logfile:
        start_args['log_file'] = options.logfile
        bglogger = queuelogger.setup_bg_logging(options.logfile, debuglevel)
    else:
        logging.getLogger().setLevel(debuglevel)
    logging.info("Starting Klippy...")
    # 获取git未跟踪的文件列表和已修改文件列表打印到日志
    git_info = util.get_git_version()
    git_vers = git_info["version"]
    # git未跟踪的文件列表
    extra_files = [fname for code, fname in git_info["file_status"]
                   if (code in ('??', '!!') and fname.endswith('.py')
                       and (fname.startswith('klippy/kinematics/')
                            or fname.startswith('klippy/extras/')))]
    # git修改的文件列表
    modified_files = [fname for code, fname in git_info["file_status"]
                      if code == 'M']
    extra_git_desc = ""
    if extra_files:
        if not git_vers.endswith('-dirty'):
            git_vers = git_vers + '-dirty'
        if len(extra_files) > 10:
            extra_files[10:] = ["(+%d files)" % (len(extra_files) - 10,)]
        extra_git_desc += "\nUntracked files: %s" % (', '.join(extra_files),)
    if modified_files:
        if len(modified_files) > 10:
            modified_files[10:] = ["(+%d files)" % (len(modified_files) - 10,)]
        extra_git_desc += "\nModified files: %s" % (', '.join(modified_files),)
    extra_git_desc += "\nBranch: %s" % (git_info["branch"])
    extra_git_desc += "\nRemote: %s" % (git_info["remote"])
    extra_git_desc += "\nTracked URL: %s" % (git_info["url"])
    start_args['software_version'] = git_vers
    start_args['cpu_info'] = util.get_cpu_info()
    if bglogger is not None:
        versions = "\n".join([
            "Args: %s" % (sys.argv,),
            "Git version: %s%s" % (repr(start_args['software_version']),
                                   extra_git_desc),
            "CPU: %s" % (start_args['cpu_info'],),
            "Python: %s" % (repr(sys.version),)])
        logging.info(versions)
    elif not options.debugoutput:
        logging.warning("No log file specified!"
                        " Severe timing issues may result!")
    # 禁用自动垃圾回收，由后续的主循环主动调用collect垃圾回收
    gc.disable()

    # 进入主循环，启动Printer()类
    while 1:
        if bglogger is not None:
            bglogger.clear_rollover_info()
            bglogger.set_rollover_info('versions', versions)
        gc.collect()
        # 事件轮训监听器，定时器处理器
        main_reactor = reactor.Reactor(gc_checking=True)
        # 创建Printer对象
        printer = Printer(main_reactor, bglogger, start_args)
        # 执行事件轮训
        res = printer.run()
        if res in ['exit', 'error_exit']:
            break
        time.sleep(1.)
        main_reactor.finalize()
        main_reactor = printer = None
        logging.info("Restarting printer")
        start_args['start_reason'] = res

    if bglogger is not None:
        bglogger.stop()

    if res == 'error_exit':
        sys.exit(-1)

if __name__ == '__main__':
    main()
